package com.gubatian.dfs.namenode.server;

import java.util.LinkedList;
import java.util.List;

/**
 * 负责管理edit log日志的核心组件
 *
 * @author: gubatian
 * @create: 2021-09-22 18:39
 */
public class FSEditLog {
    /**
     * 当前递增到txid的序号
     */
    private long txidSeq = 0L;
    /**
     * 内存双缓冲区
     */
    private DoubleBuffer editLogBuffer = new DoubleBuffer();
    /**
     * 当前是否正在将内存数据输入磁盘中
     */
    private volatile Boolean isSyncRunning = Boolean.FALSE;
    /**
     * 当前是否有线程等待刷入下一批缓冲数据到磁盘文件中去
     */
    private volatile Boolean isWaitSync = Boolean.FALSE;
    /**
     * 在同步磁盘文件中最大的全局ID
     */
    private volatile Long syncMaxTxid = 0L;
    /**
     * 每个线程都有本地的txid
     */
    private ThreadLocal<Long> localTxid = new ThreadLocal<>();
    /**
     * 负责记录edit log日志
     * @param log 日志
     */
    public void logEdit(String log) {
        // 这里必须得直接加锁
        synchronized (this) {
            // 获取全局唯一的txid，代表edits log的序号
            txidSeq ++;
            long txid = txidSeq;
            // txid设置给线程副本
            localTxid.set(txid);
            // 构建一条edits log对象
            EditLog editLog = new EditLog(txid, log);
            // 将edit log写入内存缓冲区去，而不是直接刷入磁盘
            editLogBuffer.write(editLog);
        }

        logSync();
    }

    /**
     * 将内存缓冲中的数据刷入磁盘文件中
     * 在这里允许某一个线程一次性把内存缓冲中的数据刷入磁盘文件中
     * 相当于实现一个批量把内存数据刷入磁盘的操作
     */
    private void logSync() {
        // 再次加锁
        synchronized (this) {
            // 如果正好有线程将缓冲数据刷入磁盘中
            if (isSyncRunning) {
                // 此时应该做一些逻辑判断

                // 加入某个线程把txid=1,2,3,4,5的edits log刷入磁盘了，或者正在刷入磁盘中
                // 那么对应的txid = 5，代表的是正在刷入内存的最大txid
                // 这是后来了一个localTxid = 3的线程，
                // 说明该线程对应的edits log已经被刷入磁盘中，或正在刷入磁盘
                // 则该线程就不需要在这等待
                long txid = localTxid.get();
                if (txid <= syncMaxTxid) {
                    return;
                }
                // 此时来了一个txid = 9的线程，如果它发现已经有正在刷入磁盘的线程，
                // 那么它就直接返回去处理其他事情
                // 假如说此时来一个txid = 6的线程，那么的话，他是不好说的
                // 他就需要做一些等待，同时要释放掉锁
                if (isWaitSync) {
                    return;
                }
                // 比如说此时可能是txid=15的线程再次等待
                isWaitSync = true;
                while (isSyncRunning) {
                    try {
                        wait(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                isWaitSync = false;
            }
            // 交换两块缓冲区
            editLogBuffer.setReadyToSync();
            // 记录一下刷入到内存中的最大txid
            // 此时editLogBuffer的syncBuffer缓冲区中可能有多条记录
            // 而且他的txid一定是从小到大的
            // 此时要同步的数据txid=6,7.8,9,10,11,12
            // 那么txid=12
            syncMaxTxid = editLogBuffer.getSyncMaxTxid();
            // 设置当前正在同步到磁盘的标志位
            isSyncRunning = true;
        }
        // 开始把内存刷入磁盘
        editLogBuffer.flush();
        synchronized (this) {
            // 同步磁盘完后，恢复正在同步的标志位，再释放锁
            isSyncRunning = false;
            // 唤醒正在等待同步刷完磁盘的线程
            notifyAll();
        }
    }
    class EditLog {
        long txid;
        String content;

        public EditLog(long txid, String content) {
            this.txid = txid;
            this.content = content;
        }

    }

    class DoubleBuffer {
        /**
         * 专门用来承载线程写入edits log
         */
        private LinkedList<EditLog> currentBuffer = new LinkedList<>();
        /**
         * 专门用来把内存数据同步到磁盘的一块缓冲
         */
        private LinkedList<EditLog> syncBuffer = new LinkedList<>();

        /**
         * 写入edits log到内存缓冲中去
         * @param editLog edits log
         */
        private void write(EditLog editLog) {
            currentBuffer.add(editLog);
        }

        /**
         * 交换两块缓冲区，为同步数据到磁盘做准备
         */
        private void setReadyToSync() {
            LinkedList<EditLog> tmp = syncBuffer;
            syncBuffer = currentBuffer;
            currentBuffer = tmp;
        }

        /**
         * 同步缓冲区数据刷入磁盘文件
         */
        private void flush() {
            for (EditLog log : syncBuffer) {
                System.out.println("将edits log写入磁盘文件：" + log);
                // 这块应该用输出流写入磁盘文件
            }
            // 刷入磁盘完成后清空同步缓冲区
            syncBuffer.clear();
        }
        private Long getSyncMaxTxid() {
            return syncBuffer.getLast().txid;
        }
    }

}